﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using metroioc.Container;

namespace MetroIoc
{
    partial class MetroContainer
    {
        /// <summary>
        /// Resolve an instance of the specified interface (or class) Type
        /// </summary>
        /// <typeparam name="T">The type of interface or class to be resolved</typeparam>
        /// <param name="key">(Optional) a key to specify the instance within the container</param>
        /// <returns>The registered instance if key is specified, or a dynamically instantiated instance, if not</returns>
        public T Resolve<T>(string key = null)
        {
            return (T)Resolve(typeof(T), key);
        }

        /// <summary>
        /// Try to resolve an instance of the specified interface (or class) Type
        /// </summary>
        /// <typeparam name="T">The type of interface or class to be resolved</typeparam>
        /// <param name="key">(Optional) a key to specify the instance within the container</param>
        /// <returns>An instance of <typeparamref name="T"/> if registered, or null</returns>
        public T TryResolve<T>(string key) where T : class
        {
            return (T)TryResolve(typeof(T), key);
        }

        /// <summary>
        /// Resolve an instance of the specified interface (or class) Type
        /// </summary>
        /// <param name="type">The type of interface or class to be resolved</param>
        /// <param name="key">(Optional) a key to specify the instance within the container</param>
        /// <returns>The registered instance if key is specified, or a dynamically instantiated instance, if not</returns>
        public object Resolve(Type type, string key = null)
        {
            var result = ResolveInstance(type, key, scope);
            BuildUp(result);
            return result;
        }

        /// <summary>
        /// Try to resolve an instance of the specified interface (or class) Type
        /// </summary>
        /// <param name="type">The type of interface or class to be resolved</param>
        /// <param name="key">(Optional) a key to specify the instance within the container</param>
        /// <returns>An instance of  if registered, or null</returns>
        public object TryResolve(Type type, string key)
        {
            try
            {
                return Resolve(type, key);
            }
            catch
            {
                return null;
            }
        }

        /// <summary>
        /// Resolve all registered instances of a specified type
        /// </summary>
        /// <typeparam name="T">The type of interface or class to be resolved</typeparam>
        /// <returns>A collection of registered instances. If no instances are registered, returns empty collection, not null</returns>
        public IEnumerable<T> ResolveAll<T>()
        {
            var allObjects = ResolveAll(typeof(T));

            return allObjects.Select(obj => (T)obj);
        }

        /// <summary>
        /// Resolve all registered instances of a specified type
        /// </summary>
        /// <param name="type">The type of interface or class to be resolved</param>
        /// <returns>A collection of registered instances. If no instances are registered, returns empty collection, not null</returns>
        public IEnumerable<object> ResolveAll(Type type)
        {
            return ResolveAllInternal(type, scope);
        }

        static IEnumerable<object> ResolveAllInternal(Type type, LifetimeScope lifetimeScope)
        {
            return lifetimeScope.Registrations
                .Select(r => r.Key)
                .Where(key => key.Item1 == type)
                .Select(t => lifetimeScope.Get(t).Resolve(t.Item1, t.Item2, lifetimeScope));
        }

        private object ResolveInstance(Type type, string key, LifetimeScope lifetimeScope)
        {
            if (type == null)
            {
                type = lifetimeScope.DeriveType(key);
                if (type == null)
                    throw new ResolutionException("Failed to Derive type for " + key);
            }

            key = key.ValueOrDefault(CollectionDefaultKey);
            TypeResolver r;
            if (!lifetimeScope.TryGetValue(new Tuple<Type, string>(type, key), out r))
                r = new TypeResolver
                {
                    Registration = new Transient(),
                    ResolveInstance = res => InstantiateInstance(type, res)
                };

            return r.Resolve(type, key, lifetimeScope);
        }

        private object InstantiateInstance(Type type, LifetimeScope lifetimeScope)
        {
            var constructor = GetConstructorForType(type);

            if (constructor == null)
                throw new ResolutionException("Could not locate a constructor for " + type.FullName);

            var constructorParams = new List<object>(constructor.GetParameters().Length);
            foreach (var parameterInfo in constructor.GetParameters())
            {
                object parameter = null;
                try
                {
                    string key = type.ConstructorParamPattern(parameterInfo.Name);
                    parameter = ResolveInstance(null, key, lifetimeScope);
                }
                catch (Exception)
                {
                    parameter = ResolveInstance(parameterInfo.ParameterType, null, lifetimeScope);
                }
                finally
                {
                    constructorParams.Add(parameter);
                }
            }

            try
            {
                return constructor.Invoke(constructorParams.ToArray());
            }
            catch (Exception exception)
            {
                throw new ResolutionException("Failed to resolve " + type.FullName, exception);
            }
        }

        private ConstructorInfo GetConstructorForType(Type type)
        {
            var allConstructors = type.GetTypeInfo()
                .DeclaredConstructors
                .Where(c => c.IsPublic)
                .ToList();

            var preferredConstructor = allConstructors
                .SingleOrDefault(c => c.GetCustomAttribute<PreferredConstructorAttribute>() != null);
            if (preferredConstructor != null)
                return preferredConstructor;

            return allConstructors
                .OrderByDescending(c => c.GetParameters().Length)
                .FirstOrDefault();
        }
    }
}
